﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;


using FernUniHagen.CompilerExtension.DataStructures;
using Roslyn.Compilers;
using Roslyn.Compilers.Common;
using Roslyn.Compilers.CSharp;
using Roslyn.Services;
using Roslyn.Services.CSharp;
using Roslyn.Services.Formatting;
using FernUniHagen.CompilerExtension.Attributes;
using FernUniHagen.CompilerExtension.Functions;
using FernUniHagen.CompilerExtension.Adaptor;
using FernUniHagen.CompilerExtension.Values;
using FernUniHagen.CompilerExtension.Interfaces;

/*TODO List:
1.a Remove the Multiplicity attributes from fields etc. 
1. Consider a design, where the semantic model is constantly rebuild
3. DeclarationAdapter: Method GetType returned 'var' if var was used in declaration
4. Declaration Check: Check if multiplicity rules apply in declaration: e.g. var test = anyField
5. FieldDeclaration: Multiple Variable Names not supported e.g.: string a,b
6. R-Return2 should use MCast
7. Problem with Linq Query...Symbol won't be replaced
8. Check if BlockSyntaxAdapter can be rewriten with the new replacement strategy
9. Consider a design where the Declarations return the multiplicity type and if valid the generic type
10.There is always the identifier taken and replaced but there is nothing that makes sure that the correct identifier is taken
11. Roslyn.Compiler.Extension.exe could not be found. Need to copy it to the correct location
12. Return statements need a different strategy
*/
namespace FernUniHagen.CompilerExtension
{
    public class Program
    {

        private const string c_newSolutionFolder = @"c:\RoslynOutput";
        static void Main(string[] args)
        {

            var annotatedSyntaxNodes = new List<SyntaxNodeInfo>();

            //Arguments should be used to reference the csharp projekt. but if not used use a default project  
            if (args == null || args.Length == 0)
            {
                var currentAsmPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
                var path = Path.Combine(currentAsmPath, @"C:\Users\cpp00309\Documents\Fernuni\MasterArbeit\MultiplicityInput\MultiplicityInput.sln");
                args = new string[] { path };
            }

            ISolution solution = Solution.Load(args[0]);
            //Save solution to destination path
            DirectoryInfo directory = new DirectoryInfo(Path.GetDirectoryName(solution.FilePath));
            directory.CopyTo(c_newSolutionFolder,true);
           
            PerformReplacement(solution);

            //Remove annotations

            ////2. Now that we have the annotated nodes, we also need to find the places where they are referenced
            //var referencedNodes = FindAllReferences(solution, project, annotatedSyntaxNodes);
            //PreProcessor.CheckSyntaxNodes(annotatedSyntaxNodes);

            ////2.a And additionally, in case of annotated Methods we need to get the return statements of these methods
            //referencedNodes.AddRange(FindReturnStatements(solution, project, annotatedSyntaxNodes));

            ////2.b Print all references to see what we have
            //PrintAllReferences(solution, annotatedSyntaxNodes);
            //PrintAllReferences(solution, referencedNodes);

            ////3. Now that we have annotated nodes and all their referenced locations we can start to do the replacements
            //var documentNodeInfos = referencedNodes.ToLookup(l => l.Document, l => l);

            //string newVariableName = string.Empty;
            ////TODO: destination file should be updated even if there is no replacement
            ////4. Replace all block nodes 
            //ReplaceDocumentNodes(solution, compilation, documentNodeInfos, newVariableName);
            
            ////5. Save the new solution (with updated block nodes, but without updated definitions)
            //string solutionName = Path.GetFileName(solution.FilePath);
            //solution = Solution.Load(@"c:\RoslynOutput\" + solutionName);
            ////TODO: for final release we need to iterate over the projects
            ////6. Get the project again to replace the definitions
            //project = solution.Projects.Where(p => p.Name.Equals("Example.Linq")).First();
            //compilation = project.GetCompilation();
            //annotatedSyntaxNodes = GetMultiplicityAnnotatedElements(solution, project);
            //documentNodeInfos = annotatedSyntaxNodes.ToLookup(l => l.Document, l => l);
      
            ////7. Replace the definitions
            //ReplaceDocumentNodes(solution, compilation, documentNodeInfos, newVariableName);

            ////8. Finally open the solution
            //solutionName = Path.GetFileName(solution.FilePath);
            //System.Diagnostics.Process.Start(@"c:\RoslynOutput\" + solutionName);
        }

        private static void PerformReplacement(ISolution solution)
        {
            string solutionName = Path.GetFileName(solution.FilePath);
            IDocument newDoc = null;
            CommonSyntaxNode newRootNode =null;
            StatementReplacement<CommonSyntaxNode> newNode = null;
            IMultiplicityConvertible<CommonSyntaxNode> nodeAdapter = null;
            SolutionAdapter solutionAdapter;

            foreach(var inputProject in solution.Projects)
            {
                solutionAdapter = new SolutionAdapter(solution, inputProject);
                var annotatedDeclarations = AttributeWalker.GetMultiplicityAnnotatedElements(solution, inputProject);
               
                //Pick one of the annotated nodes and find all the references to it
                while (annotatedDeclarations != null && annotatedDeclarations.Count > 0)
                {
                    var annotatedNode = annotatedDeclarations.FirstOrDefault(ad =>
                        !SyntaxElementFactory.GetDeclaration(ad.NodeToReplace,
                            solutionAdapter.Compilation.GetSemanticModel(ad.NodeToReplace.SyntaxTree)).GetName().Contains("roslynDefinition"));
                    if (annotatedNode == null) break;

                    var semanticModel = solutionAdapter.Compilation.GetSemanticModel(annotatedNode.NodeToReplace.SyntaxTree);
                    var declaration = SyntaxElementFactory.GetDeclaration(annotatedNode.NodeToReplace, semanticModel);                       
                    string newVariableName = VariableCreatorService.GetDefinitionVariable(declaration.GetName());
                    var referencedNodes = FindAllReferences(solutionAdapter.RoslynSolution, solutionAdapter.CurrentProject, annotatedNode.NodeToReplace);

                    while (referencedNodes != null && referencedNodes.Count > 0)
                    {
                        var referencedNode = referencedNodes.First();
                        //Replace the node and build a new root with this replacement
                        newRootNode = referencedNode.Document.GetSyntaxRoot();
                        semanticModel = solutionAdapter.Compilation.GetSemanticModel(referencedNode.NodeToReplace.SyntaxTree);
                        nodeAdapter = SyntaxElementFactory.GetConverter(semanticModel, referencedNode.NodeToReplace, referencedNode);
                        
                        //New reference name
                        newNode = nodeAdapter.ConvertToMutiplicity(newVariableName);
                        newRootNode = newRootNode.ReplaceNode(referencedNode.NodeToReplace, newNode.NewSyntaxNode);
                        newRootNode = (SyntaxNode)newRootNode.Format(FormattingOptions.GetDefaultOptions()).GetFormattedRoot();
                        newDoc = referencedNode.Document.UpdateSyntaxRoot(newRootNode);

                        //Update the document with the replacement
                        UpdateDocument(solution, newDoc);

                        solutionAdapter = new SolutionAdapter(@"c:\RoslynOutput\" + solutionName, inputProject.Name);

                        //and reload the solution with a new semantic model
                        annotatedDeclarations = AttributeWalker.GetMultiplicityAnnotatedElements(solutionAdapter.RoslynSolution, solutionAdapter.CurrentProject);
                        annotatedNode = annotatedDeclarations.FirstOrDefault(ad =>
                            !SyntaxElementFactory.GetDeclaration(ad.NodeToReplace,
                                solutionAdapter.Compilation.GetSemanticModel(ad.NodeToReplace.SyntaxTree)).GetName().Contains("roslynDefinition"));
                        referencedNodes = FindAllReferences(solutionAdapter.RoslynSolution, solutionAdapter.CurrentProject, annotatedNode.NodeToReplace);
                    }
                    //Until now we replaced all the references with at least a new Variable Name. Now we set the definition to the new Variable Name
                    newRootNode = annotatedNode.Document.GetSyntaxRoot();
                    semanticModel = solutionAdapter.Compilation.GetSemanticModel(annotatedNode.NodeToReplace.SyntaxTree);
                    nodeAdapter = SyntaxElementFactory.GetConverter(semanticModel, annotatedNode.NodeToReplace, annotatedNode);
                
                    //Need to rename reference
                    newNode = nodeAdapter.ConvertToMutiplicity(newVariableName);
                    newRootNode = newRootNode.ReplaceNode(annotatedNode.NodeToReplace, newNode.NewSyntaxNode);
                    newRootNode = (SyntaxNode)newRootNode.Format(FormattingOptions.GetDefaultOptions()).GetFormattedRoot();
                    newDoc = annotatedNode.Document.UpdateSyntaxRoot(newRootNode);

                    //Update the document with the replacement
                    UpdateDocument(solution, newDoc);

                    //and reload the solution with a new semantic model
                    solutionAdapter = new SolutionAdapter(@"c:\RoslynOutput\" + solutionName, inputProject.Name);

                    annotatedDeclarations = AttributeWalker.GetMultiplicityAnnotatedElements(solutionAdapter.RoslynSolution, solutionAdapter.CurrentProject);
                   
                }
            }
        }



        /// <summary>
        ///
        /// Updates a document at the given folder
        /// </summary>
        /// <param name="solution"></param>
        /// <param name="newDoc"></param>
        private static void UpdateDocument(ISolution solution, IDocument newDoc)
        {
            var solutionDirectory = Path.GetDirectoryName(solution.FilePath);
            string solutionRoot = Path.GetDirectoryName(solution.FilePath);
            
            
            var documentDirectory = newDoc.FilePath;
            var newFileName = documentDirectory.Replace(solutionRoot, c_newSolutionFolder);

            if (!Directory.Exists(Path.GetDirectoryName(newFileName)))
                Directory.CreateDirectory(Path.GetDirectoryName(newFileName));
            
            File.WriteAllText(newFileName, newDoc.GetSyntaxRoot().ToFullString());
        }

        /// <summary>
        /// copys the solution to a new folder
        /// </summary>
        /// <param name="solution">the soltion where the document is in</param>
        /// <param name="project">the project where the document is in</param>
        /// <param name="newDoc">the new document</param>
        private static void SaveDocument(ISolution solution, IProject project, IDocument newDoc)
        {
            var sourceDirectory = Path.GetDirectoryName(solution.FilePath);
            var sourceDirectoryInfo = new DirectoryInfo(sourceDirectory);
            sourceDirectoryInfo.CopyTo(c_newSolutionFolder);
        }

        /// <summary>
        /// Returns the return statements for all the methods which have a multiplicity annotation
        /// </summary>
        /// <param name="solution">the current solution</param>
        /// <param name="project">the current project</param>
        /// <param name="annotatedSyntaxNodes">a list of annotated syntax nodes (can be everything as it will be filtered for methoddeclerations)</param>
        /// <returns></returns>
        private static List<SyntaxNodeInfo> FindReturnStatements(ISolution solution, IProject project, List<SyntaxNodeInfo> annotatedSyntaxNodes)
        {
            var annotatedMethods = annotatedSyntaxNodes.Where(n => n.NodeToReplace is MethodDeclarationSyntax);
            var returnStatementNodes = new List<SyntaxNodeInfo>();
            foreach (var annotatedMethod in annotatedMethods)
            {
                var method = (MethodDeclarationSyntax)annotatedMethod.NodeToReplace;
                IDocument doc = solution.GetDocument(method.Body.SyntaxTree);
                //TODO: Considers only one return statement
                var returnStatement = method.Body.DescendantNodes().First(n => n is ReturnStatementSyntax) as ReturnStatementSyntax;
                //new SyntaxNodeInfo(doc, method.Body, AttributeType.Any, returnStatement, null, null);
                var nodeInfo = new SyntaxNodeInfo(doc, method.Body, returnStatement.Expression, returnStatement as StatementSyntax,null);
                returnStatementNodes.Add(nodeInfo);
            }
            return returnStatementNodes;

        }

        /// <summary>
        /// Prints all the references where a replacement should take place
        /// </summary>
        /// <param name="solution"></param>
        /// <param name="annotatedSyntaxNodes"></param>
        private static void PrintAllReferences(ISolution solution, List<SyntaxNodeInfo> annotatedSyntaxNodes)
        {
            //    IDocument doc = solution.GetDocument(inv.SyntaxTree);
            foreach (var node in annotatedSyntaxNodes)
            {
                IDocument doc = solution.GetDocument(node.NodeToReplace.SyntaxTree);
                Console.WriteLine(string.Format("{0} {1}:{2}",
                    doc.Name,
                    node.NodeToReplace.GetLocation().GetLineSpan(false).StartLinePosition,
                    node.NodeToReplace.ToString()));
                Console.WriteLine("------------------------------------------");
            }
        }

        /// <summary>
        /// returns the usages of the fields, properties etc. which were found 
        /// </summary>
        /// <param name="solution">the solution analyzed</param>
        /// <param name="project">the project analyzed</param>
        /// <param name="annotatedSyntaxNodes">the nodes which are annotated</param>
        /// <returns>list of syntax node infos (all kind of information about the elements found)</returns>
        private static List<SyntaxNodeInfo> FindAllReferences(ISolution solution, IProject project, CommonSyntaxNode declaration)
        {
            CommonCompilation compilation = project.GetCompilation();
            List<SyntaxNodeInfo> referencedNodes = new List<SyntaxNodeInfo>();

            
            ISemanticModel semanticModel = compilation.GetSemanticModel(declaration.SyntaxTree);

            if (declaration is FieldDeclarationSyntax)
            {
                FieldDeclarationSyntax fieldDeclaration = (FieldDeclarationSyntax)declaration;
                foreach (VariableDeclaratorSyntax variable in fieldDeclaration.Declaration.Variables)
                {
                    ISymbol variableSymbol = semanticModel.GetDeclaredSymbol(variable);
                    ProcessReference(solution, semanticModel, referencedNodes, variableSymbol);
                }
            }
            else if (declaration is PropertyDeclarationSyntax ||
               declaration is MethodDeclarationSyntax ||
               declaration is ParameterSyntax)
            {
                var nodeSymbol = semanticModel.GetDeclaredSymbol(declaration);
                ProcessReference(solution, semanticModel, referencedNodes, nodeSymbol);
            }
            
        
            return referencedNodes;
        }

        /// <summary>
        /// Finds the refeerences to the variable symbol and adds them to the referencedNodeList
        /// </summary>
        /// <param name="solution">the solution where the references are checked</param>
        /// <param name="semanticModel">the semantic model (only used to find the declaration of linq statements)</param>
        /// <param name="referencedNodes">the reference node list</param>
        /// <param name="variableSymbol">the symbol to find the references for</param>
        private static void ProcessReference(ISolution solution, ISemanticModel semanticModel, List<SyntaxNodeInfo> referencedNodes, ISymbol variableSymbol)
        {
            IEnumerable<ReferencedSymbol> referencedSymbols = variableSymbol.FindReferences(solution);

            foreach (var referencedSymbol in referencedSymbols)
            {
                var syntaxTokens = from reference in variableSymbol.FindReferences(solution)
                                   from loc in reference.Locations
                                   let position = loc.Location.SourceSpan.Start
                                   select loc.Location.SourceTree.GetRoot().FindToken(position) into token
                                   select token;
                foreach (var token in syntaxTokens)
                {
                    var reference = BlockNodeFactory.GetReference(solution, semanticModel ,token);
                    var nodeInfo = reference.GetReference();
                    referencedNodes.Add(nodeInfo);

                    ////All nodes found as a reference are in a block. This whole block we need to replace, so store it
                    //BlockSyntax blockSyntax = token.Parent.FirstAncestorOrSelf<BlockSyntax>();
                    ////Get the document where the node needs to be replaced
                    //IDocument doc = solution.GetDocument(blockSyntax.SyntaxTree);
                    ////The expression we want to change is also in a statement, store this so we can find the location in the block we want to replace
                    //StatementSyntax stmt = token.Parent.FirstAncestorOrSelf<StatementSyntax>();
                   
                    ////Get the expression which needs to be replaced and where we can apply our rules on
                    //BinaryExpressionSyntax bin = token.Parent.FirstAncestorOrSelf<BinaryExpressionSyntax>();
                    //var argument = token.Parent.FirstAncestorOrSelf<ArgumentSyntax>();
                    //MemberAccessExpressionSyntax mem = token.Parent.FirstAncestorOrSelf<MemberAccessExpressionSyntax>();
                    //InvocationExpressionSyntax inv = token.Parent.FirstAncestorOrSelf<InvocationExpressionSyntax>();
                    //CastExpressionSyntax castExpression = token.Parent.FirstAncestorOrSelf<CastExpressionSyntax>();
                    //QueryExpressionSyntax queryExpression = token.Parent.FirstAncestorOrSelf<QueryExpressionSyntax>();
                    //IdentifierNameSyntax identifier = token.Parent.FirstAncestorOrSelf<IdentifierNameSyntax>();
                    //ElementAccessExpressionSyntax elementAccessExpression = token.Parent.FirstAncestorOrSelf<ElementAccessExpressionSyntax>();
                    

                    ////TODO: Need to think about the right order as a statement can be multiple syntaxes at the same time
                    //if (argument != null)
                    //{
                    //    //TODO:Check if attribute conversion is necessary with Steimann (STRIP)
                    //    var nodeInfo = new SyntaxNodeInfo(doc, blockSyntax, identifier, stmt, null, identifier);
                    //    referencedNodes.Add(nodeInfo);
                    //}
                    //else if(elementAccessExpression != null)
                    //{
                    //    SyntaxNodeInfo nodeInfo = new SyntaxNodeInfo(doc, blockSyntax, elementAccessExpression, stmt, null,identifier);
                    //    referencedNodes.Add(nodeInfo);
                    //}
                    //else if (mem != null)
                    //{
                    //    if (mem.Name == identifier)
                    //    {
                    //        SyntaxNodeInfo nodeInfo2 = new SyntaxNodeInfo(doc, blockSyntax, identifier, stmt, null, identifier);
                    //        referencedNodes.Add(nodeInfo2);
                    //    }
                    //    else
                    //    {
                    //        //Take only those references where the annotated element is on the left side of the dot.
                    //        //e.g.: if a in a.b is annotated take it, if b in a.b is annotated do not take it as it should not be transformed
                    //        SyntaxNodeInfo nodeInfo = new SyntaxNodeInfo(doc, blockSyntax, mem, stmt, null, identifier);
                    //        referencedNodes.Add(nodeInfo);
                    //        //}
                    //    }
                    //}

                    ////Needed to remove bin.Right for a += a
                    //else if (bin != null /*&& bin.Right != identifier */)
                    //{
                    //    if(bin.Right == identifier)
                    //    { 
                    //        SyntaxNodeInfo nodeInfo2 = new SyntaxNodeInfo(doc, blockSyntax, identifier, stmt, null, identifier);
                    //        referencedNodes.Add(nodeInfo2);
                    //    }else { 

                    //        SyntaxNodeInfo nodeInfo = new SyntaxNodeInfo(doc, blockSyntax, bin, stmt, null, identifier);
                    //        referencedNodes.Add(nodeInfo);
                    //    }
                    //}
                    //else if (inv != null)
                    //{
                    //    SyntaxNodeInfo nodeInfo = new SyntaxNodeInfo(doc, blockSyntax, inv, stmt, null, identifier);
                    //    referencedNodes.Add(nodeInfo);
                    //}
                   
                    ////This branch covers the normal type cast on annotated elements (R-TCAST)
                    //else if (castExpression != null)
                    //{
                    //    SyntaxNodeInfo nodeInfo = new SyntaxNodeInfo(doc, blockSyntax, castExpression, stmt, null, identifier);
                    //    referencedNodes.Add(nodeInfo);
                    //}
                    
                    
                    ////A querExpression will replace a whole class because we need to implement ienumerable interface. do that here
                    //else if (queryExpression != null)
                    //{
                    //    //first get the identifier and its declaration (a field or a property)
                    //    var identifierName = (IdentifierNameSyntax)queryExpression.DescendantNodes().First(p => p is IdentifierNameSyntax);
                    //    if (identifierName == null) throw new Exception("Could not find Identifier in Linq Statement");
                    //    CommonSyntaxNode decleration = semanticModel.FindDecleration(identifierName);

                    //    //Now get the type of this declaration
                    //    TypeSyntax typeOfDeclaration = null;
                    //    if (decleration is FieldDeclarationSyntax)
                    //        typeOfDeclaration = (TypeSyntax)((FieldDeclarationSyntax)decleration).Declaration.DescendantNodes().First(p => p is TypeSyntax);
                    //    else if (decleration is PropertyDeclarationSyntax)
                    //        typeOfDeclaration = (TypeSyntax)((PropertyDeclarationSyntax)decleration).Type;
                    //    else throw new Exception("The Identifier in the Linq statement is currently not supported");

                    //    //And finally get the declaration of this type which will be the class which we want to change
                    //    var typeIdentifierName = (IdentifierNameSyntax)typeOfDeclaration.DescendantNodesAndSelf().First(n => n is IdentifierNameSyntax);
                    //    SyntaxNode typeClass = (SyntaxNode)semanticModel.FindDecleration( typeIdentifierName);

                    //    //  SyntaxNodeInfo nodeInfo = new SyntaxNodeInfo(doc, typeClass, AttributeType.Any, queryExpression, stmt, null);
                    //    SyntaxNodeInfo nodeInfo = new SyntaxNodeInfo(doc, blockSyntax, queryExpression, stmt, null, identifier);
                    //    referencedNodes.Add(nodeInfo);
                    //}
                    //else
                    //{
                    //    SyntaxNodeInfo nodeInfo = new SyntaxNodeInfo(doc, blockSyntax, identifier, stmt, null, identifier);
                    //    referencedNodes.Add(nodeInfo);
                    //}


                }

            }
        }

    

       

    }
}
